package pl.cezary.butler.walili.server.db;

import java.io.*;
import pl.cezary.butler.walili.server.db.tasks.DbTask;
import pl.cezary.butler.walili.server.db.tasks.NonConsistentTask;
import pl.cezary.butler.walili.server.db.tasks.GetTasksByDateTask;
import pl.cezary.butler.walili.server.db.utils.ConnectionPool;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.apache.tomcat.dbcp.dbcp.BasicDataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import pl.cezary.butler.walili.server.config.ServerConfiguration;
import pl.cezary.butler.walili.server.db.tasks.TableExistsTask;

/*
 * @author Przemysław Bałdyga
 */
public abstract class Database {

    private static final Logger log = Logger.getLogger("BazaDanych");
    private boolean fail = false;
    private static final int EXECUTORSNO = 1;
    private static final int EXECUTOR_GLOWNY = 0;
    private static final int EXECUTOR_NIESPOJNE = 1;
    private static final int EXECUTOR_GET_BY_DATE = 2;
    private static final Map<Integer, String> execNames = new TreeMap<Integer, String>();
    
    private boolean closing = false;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }        
    
    //private static final int CONNECTION_NO = 5;
    //private ConnectionPool connectionPool = null;
    
    public void startClosing(){
        closing=true;
    }

    static {
        execNames.put(EXECUTOR_GLOWNY, "główny");
        /*execNames.put(EXECUTOR_NIESPOJNE, "niespójne");
        execNames.put(EXECUTOR_GET_BY_DATE, "po dacie");*/
    }
    private DbExecutor[] executors;

    public Database() {
    }

    protected Database(BasicDataSource ds) {
        this.ds = ds;
    }

    private void createExecutors() {
        if (executors == null) {
            executors = new DbExecutor[EXECUTORSNO];
            for (int i = 0; i < EXECUTORSNO; ++i) {
                executors[i] = new DbExecutor(new LinkedBlockingQueue<DbTask>(), name + ": " +execNames.get(i), getDataSource(), NullIdFactory.getInstance());
            }
        }
    }
        
    private void executorsStart() {
        createExecutors();
        log.info("Executors starting");        
        for (DbExecutor e : executors) {
            e.start();
        }
    }

    private void executorsStop() throws InterruptedException {
        log.info("Executors stopping");
        for (DbExecutor e : executors) {
            e.interrupt();
        }
        for (DbExecutor e : executors) {
            e.join();
        }
    }

    public static Database getMySqlInstance() {
        return DatabaseMySql.getInstance();
    }

    public static Database getSqliteInstance() {
        return DatabaseSqlite.getInstance();
    }

    public static Database getHyperSqlInstance() {
        return DatabaseHyperSql.getInstance();
    }

    public static Database getInstance() {
        final String type = ServerConfiguration.getInstance().getDbType();
        if ("sqlite".equals(type)) {
            return getSqliteInstance();
        } else if ("mysql".equals(type)) {
            return getMySqlInstance();
        } else if ("hypersql".equals(type)) {
            return getInstance();
        }
        return getSqliteInstance();
    }

    //protected abstract Connection createConnection() throws RuntimeException;
    public void polacz() {
        /*
         * if (connectionPool != null) { return; } getDriverClass();
         * List<Connection> pool = new ArrayList<Connection>(CONNECTION_NO); for
         * (int i = 0; i < CONNECTION_NO; ++i) { pool.add(createConnection()); }
         * connectionPool = new ConnectionPool(pool);
         */
        executorsStart();
        //wykonajZadanieBD(new ZadanieInicjalizacjaBD()); po co, czy to wogóle potrzebne i czy to dobry pomysł
    }

    public void executeDbTask(DbTask zadanie) {
        if(closing){                        
            try{
                throw new RuntimeException("Submited task");
            }catch(RuntimeException re){
                log.log(Level.WARNING,"Datbase is being prepared for close but task {0} was submited.",zadanie);
                log.log(Level.WARNING, "Stack for source of task", re);
            }
        }
        
        //TODO routing to executors should be configured externaly as well as executros number and kind
        /*if (zadanie instanceof GetTasksByDateTask) {
            executors[EXECUTOR_GET_BY_DATE].getQueue().offer(zadanie);
        } else if (zadanie instanceof NonConsistentTask) {
            executors[EXECUTOR_NIESPOJNE].getQueue().offer(zadanie);
        } else {*/
            executors[EXECUTOR_GLOWNY].getQueue().offer(zadanie);
        //}
    }

    /*
     * private void wykonajZadanieBD_internal(final DbTask zadanie) { assert
     * (connectionPool != null) : "Wykonywanie zadan dozwolone jest dopiero po
     * polaczeniu"; zadanie.setDataBase(this); zadanie.run();        
    }
     */
    public void rozlacz() {
        try {
            executorsStop();
            /*
             * if (connectionPool == null) { return; } connectionPool.zamknij();
             * connectionPool = null;
             */
            if (ds != null) {
                try {
                    ds.close();
                } catch (SQLException ex) {
                    throw new RuntimeException("Problem while trying to close connection pool", ex);
                }
            }
        } catch (InterruptedException ex) {
            //none
        }
    }

    @Override
    protected void finalize() throws Throwable {
        rozlacz();
        super.finalize();
    }

    /*
     * public Connection getConection() { return connectionPool.podaj();
    }
     */
    void releaseConection(Connection c) {
        try {
            c.close();
        } catch (SQLException ex) {
            Logger.getLogger(Database.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void fail() {
        log.severe("Błąd spowodował oznaczenie bazy jako niezdatnej do dalszego użycia");
        fail = true;
    }

    public boolean getFailStatus() {
        return fail;
    }

    public InputStream getBuilderStream() {
        final String resName = Database.class.getPackage().getName().replace('.', '/') + "/" + getDbInitPath();
        log.log(Level.INFO, "Próba pobrania zasobu {0}", resName);
        return ClassLoader.getSystemResourceAsStream(resName);
    }

    protected String getDbInitPath() {
        return "builder.sql";
    }
    
    protected void initDb(){
        initDb(getBuilderStream());
    }
    
    protected void initDb(String res){
        initDb(ClassLoader.getSystemResourceAsStream(res));
    }

    protected void initDb(InputStream input) {
        
        assert input != null;
        final InputStreamReader iReader = new InputStreamReader(input);
        final BufferedReader bufferedReader = new BufferedReader(iReader);        
        final JdbcTemplate template = new JdbcTemplate(getDataSource());        
        try {
            StringBuilder queryString = new StringBuilder();
            String line;
       
            try {
                while ((line = bufferedReader.readLine()) != null) {
                    line = line.trim();
                    if (line.startsWith("--")) {
                        continue;
                    }
                    if (queryString.length() > 0) {
                        queryString.append((char) Character.LINE_SEPARATOR);
                    }
                    queryString.append(line);
                    if (line.endsWith(";")) {
                        final String sqlString = queryString.toString();
                        log.log(Level.INFO, "Pełne zapytanie gotowe {0} ", sqlString);
                        
                        try {
                            template.update(sqlString);                            
                        } catch (DataAccessException ex) {
                            log.log(Level.SEVERE, String.format("Błąd podczas wykonywania zapytania %s", sqlString), ex);
                            throw new RuntimeException(ex);
                        }
                        queryString.delete(0, queryString.length());
                    }
                }
            } catch (IOException ex) {
                log.log(Level.SEVERE, "Błąd odczytu pliku definicji schematu", ex);
            }

        } finally {
            try {
                bufferedReader.close();
                iReader.close();
                input.close();
            }  catch (IOException ex) {
                Logger.getLogger(Database.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

    }

    void joinExecutors() {
        for (int i = 0; i < EXECUTORSNO; ++i) {
            while (!executors[i].isQueueEmpty()) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException ex) {
                    log.log(Level.INFO, null, ex);
                }
            }
        }
    }

    protected abstract String getConnectionUrl();

    protected abstract String getUsername();

    protected abstract String getPassword();

    protected abstract Class<? extends Driver> getDriverClass();
    private BasicDataSource ds = null;

    public DataSource getDataSource() {
        if (ds == null) {
            BasicDataSource localDs = new BasicDataSource();

            localDs.setUrl(getConnectionUrl());
            localDs.setUsername(getUsername());
            localDs.setPassword(getPassword());
            localDs.setDriverClassName(getDriverClass().getName());
            localDs.setTestWhileIdle(true);
            localDs.setTestOnBorrow(false);
            localDs.setTestOnReturn(false);
            localDs.setDefaultTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            //localDs.setValidationQuery("SELECT testStr FROM test_tab");
            if (true) {
                localDs.setMaxActive(EXECUTORSNO);
                localDs.setMaxIdle(EXECUTORSNO);
                localDs.setMaxWait(EXECUTORSNO);
            }
            ds = localDs;
        }

        return ds;
    }
    
    private String tableCheck;

    public String getTableCheck() {
        return tableCheck;
    }

    public void setTableCheck(String tableCheck) {
        this.tableCheck = tableCheck;
    }
    
    protected boolean isInitialized(){
        return true;
        /*
        TableExistsTask task = new TableExistsTask(tableCheck);
        executeDbTask(task);
        return task.getResults();*/
    }
 
    public void setInitScript(String initScript) {
        if(!isInitialized())
            initDb(initScript);
    }
            
    
    
}
